package ganga.custodian.sensors;

import org.hermit.tricorder.GeoElement;
import org.hermit.tricorder.SatelliteElement;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.RectF;
import android.hardware.GeomagneticField;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

import ganga.custodian.CustodianApplication;
import ganga.custodian.R;

/**
 * Монитор положения. Отвечает за получение данных от службы местоположения.
 * Виртуализирует датчик положения.
 * @author Bacherikov, original from Tricorder.GeoView 
 * @version 0.3
 * @updated 19-июл-2011 16:27:34
 */
public class LocationMonitor extends Service 
implements GpsStatus.Listener, LocationListener, SensorEventListener
{
	/**
	 * Конфигурационные параметры
	 * @author Apocrypha
	 */
	public static class Conifg {
		/**
		 * Минимальное время между обновлениями в мс
		 */
		public int locationUpdatesMinTime;
		/**
		 * Минимальное расстояние в метрах, при перемещении на которое выполняется обновление
		 */
		public float locationUpdatesMinDistance;
		// Time in ms for which cached geomagnetic data is valid.
		private static final int GEOMAG_CACHE_TIME = 2 * 3600 * 1000;
		// Time in ms for which cached satellite data is valid.
		private static final int DATA_CACHE_TIME = 10 * 1000;

		public Conifg() {
			locationUpdatesMinTime = 60 * 1000;
			locationUpdatesMinDistance = 0f;
		}
	}

	public LocationMonitor() {
		super();
		application = CustodianApplication.getInstance();
		sensorManager = application.sensorManager;
		locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

		initializeSatelliteCache();
		
		//from surfer runner
        //Get some UI strings.
        //Vic msgDisabled = application.getRes(R.string.msgDisabled);
        msgOffline = application.getString(R.string.msgOffline);
	}
	/**
	 * Set up the satellite data cache.
	 */
	private void initializeSatelliteCache() {
		//For simplicity, we allocate NUM_SATS + 1 so we can index by PRN number.
		satCache = new GpsInfo[NUM_SATS + 1];
		for (int i = 1; i <= NUM_SATS; ++i)
			satCache[i] = new GpsInfo(i);
		numSats = 0;
	}

	private CustodianApplication application;
	// The location manager, from which we get location updates.
	private LocationManager locationManager;
	// The sensor manager, which we use to interface to all sensors.
	private SensorManager sensorManager;
    
	// Display panes for the network and GPS locations, with heading,
	// position, and course data for each.
	private GeoElement netElement;
	private GeoElement gpsElement;

	// Display pane for satellite status.  It's current bounds.
    private SatelliteElement satElement;

	// Location providers we monitor.
	private static final String[] locationProviders = {
		LocationManager.NETWORK_PROVIDER,
		LocationManager.GPS_PROVIDER
	};
	 
	// Co-ordinate transformations.
    private static final int[][] TRANSFORM_0 = {
        {  1,  0,  0 },
        {  0,  1,  0 },
        {  0,  0,  1 },
    };/*
    private static final int[][] TRANSFORM_90 = {
        {  0, -1,  0 },
        {  1,  0,  0 },
        {  0,  0,  1 },
    };
    private static final int[][] TRANSFORM_180 = {
        { -1,  0,  0 },
        {  0, -1,  0 },
        {  0,  0,  1 },
    };
    private static final int[][] TRANSFORM_270 = {
        {  0,  1,  0 },
        { -1,  0,  0 },
        {  0,  0,  1 },
    };*/

	private Conifg config = new Conifg();
	// Latest GPS status.  If null, we haven't got one yet.
	public GpsStatus gpsStatus = null;
	// Cached satellite info.  Indexed by the satellite's PRN number,
	// which is in the range 1-NUM_SATS.
	private GpsInfo[] satCache;
    /**
     * Number of GPS satellites we can handle.  Satellites are numbered 1-32;
     * this is the "PRN number", i.e. a number which identifies the
     * satellite's PRN, which is a 1023-bit number.
     */
    public static final int NUM_SATS = 32;
	// Number of satellites for which we have info.
	private int numSats;

	// The most recent accelerometer and compass data.
    private float[] accelValues = null;
    private float[] magValues = null;
    // Some useful strings.
    //Vic private final String msgDisabled;
    private final String msgOffline;

    /**
     * Cached info on a satellite's status.
     */
    public static final class GpsInfo {
        GpsInfo(int prn) {
            this.prn = prn;
            this.name = "" + prn;
        }
        
        final int prn;
        public final String name;
        
        // Time at which this status was retrieved.  If 0, not valid.
        public long time = 0;
        
        public float azimuth;
        public float elev;
        public float snr;
        boolean hasAl;
        boolean hasEph;
        boolean used;
        
        // Time at which this satellite was last used in a fix.
        long usedTime = 0;
        
        // Bounding rectangle for the display in the sky view.
        public RectF rect;
        
        // Label position for the display in the sky view.
        public float textX;
        public float textY;
        
        // Colour to plot it with, based on status.
        public int colour;
    }//class GpsInfo

    // The most recent network and GPS locations.
    private Location netLocation = null;
    private Location gpsLocation = null;

    // Current device orientation, as a matrix which can be used
    // to correct sensor input.
    private int[][] deviceTransformation = TRANSFORM_0;
    
    // Current geomagnetic data, and the time at which it was fetched.
    // null if it hasn't been got yet.
    private GeomagneticField geomagneticField = null;
    private long geomagneticTime = 0;

    // Debugging tag.
	private static final String TAG = "LocationMonitor";

    // Methods	
	/**
	 * Start this view.  This notifies the view that it should start
	 * receiving and displaying data.  The view will also get tick events
	 * starting here.
	 */
	void startGPSSensor() {
        // Register for location updates.
        for (String name : locationProviders) {
        	LocationProvider provider = locationManager.getProvider(name);
        	if (provider != null) {
        		locationManager.requestLocationUpdates(name, config.locationUpdatesMinTime, config.locationUpdatesMinDistance, this);
        		
        		// For the GPS, also add a GPS status listener to get
        		// additional satellite and fix info.
        		if (name.equals(LocationManager.GPS_PROVIDER))
        		    locationManager.addGpsStatusListener(this);
        		
        		// Prime the pump with the last known location.
        		Location prime = locationManager.getLastKnownLocation(name);
        		if (prime != null)
        			onLocationChanged(prime);
        	}
        }
        
        // Get orientation updates.
        registerSensor(Sensor.TYPE_ACCELEROMETER);
        registerSensor(Sensor.TYPE_MAGNETIC_FIELD);
	}
	private final void registerSensor(int type) {
        Sensor sensor = sensorManager.getDefaultSensor(type);
        if (sensor != null)
            sensorManager.registerListener(this, sensor,
                                           SensorManager.SENSOR_DELAY_GAME);
	}

    /**
     * Called to report changes in the GPS status.
     * 
     * @param   event           Event number describing what has changed.
     */
	@Override
	public void onGpsStatusChanged(int event) {
	    try {
	        switch (event) {
	        case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
	            gpsStatus = locationManager.getGpsStatus(gpsStatus);
	            Iterable<GpsSatellite> sats = gpsStatus.getSatellites();
	            long time = System.currentTimeMillis();
	            for (GpsSatellite sat : sats) {
	                int prn = sat.getPrn();
	                if (prn < 1 || prn > NUM_SATS)
	                    continue;

	                GpsInfo ginfo = satCache[prn];
	                ginfo.time = time;
	                ginfo.azimuth = sat.getAzimuth();
	                ginfo.elev = sat.getElevation();
	                ginfo.snr = sat.getSnr();
	                ginfo.hasAl = sat.hasAlmanac();
	                ginfo.hasEph = sat.hasEphemeris();
	                ginfo.used = sat.usedInFix();
	            }

	            //            // Fake some satellites, for testing.
	            //            Random r = new Random();
	            //            r.setSeed(4232);
	            //            for (int i = 1; i <= NUM_SATS; ++i) {
	            //                GpsInfo ginfo = satCache[i];
	            //                if (i % 3 == 0) {
	            //                    ginfo.time = time - r.nextInt(5000);
	            //                    ginfo.azimuth = r.nextFloat() * 360.0f;
	            //                    ginfo.elev = r.nextFloat() * 90.0f;
	            //                    ginfo.snr = 12;
	            //                    ginfo.hasAl = r.nextInt(4) != 0;
	            //                    ginfo.hasEph = ginfo.hasAl && r.nextInt(3) != 0;
	            //                    ginfo.used = ginfo.hasEph && r.nextBoolean();
	            //                } else {
	            //                    ginfo.time = 0;
	            //                }
	            //            }

	            // Post-process the sats.
	            numSats = 0;
	            for (int prn = 1; prn <= NUM_SATS; ++prn) {
	                GpsInfo ginfo = satCache[prn];
	                if (time - ginfo.time > Conifg.DATA_CACHE_TIME) {
	                    ginfo.time = 0;
	                    ginfo.usedTime = 0;
	                } else {
	                    if (ginfo.used)
	                        ginfo.usedTime = time;
	                    else if (time - ginfo.usedTime <= Conifg.DATA_CACHE_TIME)
	                        ginfo.used = true;
	                    else
	                        ginfo.usedTime = 0;
	                    //Vic int colour = ginfo.used ? 0 : ginfo.hasEph ? 1 : ginfo.hasAl ? 2 : 3;
	                    //Vic ginfo.colour = COLOUR_PLOT[colour];
	                    ++numSats;
	                }
	            }

	            //TODO:satElement.setValues(satCache, numSats);
	            break;
	        case GpsStatus.GPS_EVENT_STARTED:
	        case GpsStatus.GPS_EVENT_STOPPED:
	        case GpsStatus.GPS_EVENT_FIRST_FIX:
	            break;
	        }
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	}//onGpsStatusChanged

	// ******************************************************************** //
    // Location Management.
    // ******************************************************************** //
	/**
	 * Called when the location has changed.  There are no restrictions
	 * on the use of the supplied Location object.
	 * 
	 * @param	loc			   The new location, as a Location object.
	 */
	public void onLocationChanged(Location location) {
	    try {
	        synchronized (this) {
	            if (location.getProvider().equals(LocationManager.NETWORK_PROVIDER)) {
	                netLocation = location;
	              //TODO: netElement.setValue(location);
	            } else if (location.getProvider().equals(LocationManager.GPS_PROVIDER)) {
	                gpsLocation = location;
	              //TODO: gpsElement.setValue(location);
	            }
	        }
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	}
    /**
     * Called when the provider is enabled by the user.
     * 
     * @param   provider        The name of the location provider
     *                          associated with this update.
     */
    public void onProviderEnabled(String provider) {
//        try {
//            //Vic Log.i(TAG, "Provider enabled: " + provider);
//            synchronized (this) {
//                if (provider.equals(LocationManager.NETWORK_PROVIDER))
//                    netElement.setStatus(null);
//                else if (provider.equals(LocationManager.GPS_PROVIDER)) {
//                    gpsElement.setStatus(null);
//                    satElement.clearValues();
//                }
//            }
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
    }//onProviderEnabled
	/**
	 * Called when the provider is disabled by the user.
	 * If requestLocationUpdates is called on an already disabled provider,
	 * this method is called immediately.
	 * 
	 * @param	provider		The name of the location provider
	 * 							associated with this update.
	 */
	public void onProviderDisabled(String provider) {
	}
	/**
	 * Called when the provider status changes.  This method is called
	 * when a provider is unable to fetch a location or if the provider
	 * has recently become available after a period of unavailability.
	 * 
	 * @param	provider		The name of the location provider
	 * 							associated with this update.
	 * @param	status			OUT_OF_SERVICE if the provider is out of
	 * 							service, and this is not expected to
	 * 							change in the near future;
	 * 							TEMPORARILY_UNAVAILABLE if the provider
	 * 							is temporarily unavailable but is expected
	 * 							to be available shortly; and AVAILABLE if
	 * 							the provider is currently available.
	 * @param	extras			An optional Bundle which will contain
	 * 							provider specific status variables.
	 * 							Common key/value pairs:
	 * 								  satellites - the number of satellites
	 * 											   used to derive the fix.
	 */
	public void onStatusChanged(String provider, int status, Bundle extras) {
	    try {
	        Log.i(TAG, "Provider status: " + provider + "=" + status);
	        synchronized (this) {
	            String msg = null;
	            if (status == LocationProvider.OUT_OF_SERVICE)
	                msg = msgOffline;
	            if (provider.equals(LocationManager.NETWORK_PROVIDER))
	                netElement.setStatus(msg);
	            else if (provider.equals(LocationManager.GPS_PROVIDER)) {
	                gpsElement.setStatus(msg);
	            }
	        }
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	}


    // ******************************************************************** //
    // Geomagnetic Data Management.
    // ******************************************************************** //
    /**
     * Check the geomagnetic field, if the information we have isn't
     * up to date.
     */
    private void checkGeomag() {
        // See if we have valid data.
        long now = System.currentTimeMillis();
        if (geomagneticField != null && now - geomagneticTime < Conifg.GEOMAG_CACHE_TIME)
            return;
        
        // Get our best location.  If we don't have one, can't do nothing.
        final Location loc = gpsLocation != null ? gpsLocation : netLocation;
        if (loc == null)
            return;

        // Get the geomag data. 
        geomagneticField = new GeomagneticField((float) loc.getLatitude(),
                                                (float) loc.getLongitude(),
                                                (float) loc.getAltitude(), now);    
        geomagneticTime = now;
    }


    // ******************************************************************** //
    // Sensor Management.
    // ******************************************************************** //
    /**
     * Called when the accuracy of a sensor has changed.
     * 
     * @param   sensor          The sensor being monitored.
     * @param   accuracy        The new accuracy of this sensor.
     */
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // Don't need anything here.
    }
    /**
     * Called when sensor values have changed.
     *
     * @param   event           The sensor event.
     */
    public void onSensorChanged(SensorEvent event) {
    	Log.i("LocationMonitor.onSensorChanged", event.sensor.getName());
        try {
            final float[] values = event.values;
            if (values.length < 3)
                return;

            int type = event.sensor.getType();
            if (type == Sensor.TYPE_ACCELEROMETER) {
                if (accelValues == null)
                    accelValues = new float[3];
                multiply(values, deviceTransformation, accelValues);
            } else if (type == Sensor.TYPE_MAGNETIC_FIELD) {
                if (magValues == null)
                    magValues = new float[3];
                multiply(values, deviceTransformation, magValues);
            }
            checkGeomag();
            if (accelValues == null || magValues == null || geomagneticField == null)
                return;

            // Get the device rotation matrix.
            float[] rotate = new float[9];
            boolean ok = SensorManager.getRotationMatrix(rotate, null, accelValues, magValues);
            if (!ok)
                return;

            // Compute the device's orientation based on the rotation matrix.
            final float[] orient = new float[3];
            SensorManager.getOrientation(rotate, orient);

            // Get the azimuth of device Y from magnetic north.  Compensate for
            // magnetic declination.
            final float azimuth = (float) Math.toDegrees(orient[0]);
            final float dec = geomagneticField.getDeclination();
            satElement.setAzimuth(azimuth + dec, dec);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /*
     * Result[x] = vals[0]*tran[x][0] * vals[1]*tran[x][1] * vals[2]*tran[x][2].
     */
    private static final void multiply(float[] vals, int[][] tran, float[] result) {
        for (int x = 0; x < 3; ++x) {
            float r = 0;
            for (int y = 0; y < 3; ++y)
                r += vals[y] * tran[x][y];
            result[x] = r;
        }
    }

	@Override
	public IBinder onBind(Intent intent) {
		// TODO Auto-generated method stub
		return null;
	}

}